package glive.system.peer.glive;


import common.configuration.Global;
import common.peer.PeerAddress;
import common.peer.PeerDescriptor;
import common.peer.PeerInfo;
import common.peer.ViewEntry;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;
import java.util.Set;

import org.slf4j.Logger;

public class Cache {
	private Comparator<ViewEntry> comparatorByAge = new Comparator<ViewEntry>() {
		public int compare(ViewEntry o1, ViewEntry o2) {
			if (o1.getDescriptor().getAge() > o2.getDescriptor().getAge()) {
				return 1;
			} else if (o1.getDescriptor().getAge() < o2.getDescriptor().getAge()) {
				return -1;
			} else {
				return 0;
			}
		}
	};
	
	private Comparator<PeerDescriptor> comparatorByCost = new Comparator<PeerDescriptor>() {
		public int compare(PeerDescriptor o1, PeerDescriptor o2) {
			if (o1.getCost() > o2.getCost()) {
				return 1;
			} else if (o1.getCost() < o2.getCost()) {
				return -1;
			} else {
				return 0;
			}
		}
	};
	
	private Comparator<PeerDescriptor> comparatorByMoney = new Comparator<PeerDescriptor>() {
		public int compare(PeerDescriptor o1, PeerDescriptor o2) {
			if (o1.getMoney() > o2.getMoney()) {
				return 1;
			} else if (o1.getMoney() < o2.getMoney()) {
				return -1;
			} else {
				return 0;
			}
		}
	};
	
	private Comparator<PeerDescriptor> comparatorByNetProfit = new Comparator<PeerDescriptor>() {
		public int compare(PeerDescriptor o1, PeerDescriptor o2) {
//			if(o1.getPeerAddress().isSource() || o2.getPeerAddress().isSource())
//				System.out.println("testingpoint");
			float np1 = (float)self.getMoney()/o1.getCost() - (float)o1.getPrice();
			float np2 = (float)self.getMoney()/o2.getCost() - (float)o2.getPrice();
			//System.out.println(np1 + " " + np2);
			if (np1 > np2) {
				return 1;
			} else if (np1 < np2) {
				return -1;
			} else {
				if(o1.getMoney() > o2.getMoney())
					return 1;
				else if(o1.getMoney() < o2.getMoney())
					return -1;
				else
					return 0;
			}
		}
	};

//-------------------------------------------------------------------
	private final int size;
	private PeerDescriptor self;
	private PeerDescriptor parentToBeRemoved;
	private PeerDescriptor childToBeRemoved;
	private boolean costChangeFlag;
	private ArrayList<PeerDescriptor> cyclonPartners;
	private ArrayList<ViewEntry> entries;
	private HashMap<Integer, PeerDescriptor> fingersList;
	private ArrayList<PeerDescriptor> children;
	private ArrayList<PeerDescriptor> parents;
	private HashMap<PeerAddress, ViewEntry> d2e;
	private Random random;
	//Next highest market level
	private int nextML;
	private ArrayList<Integer> sortedMLs;
	private Logger logger;

//-------------------------------------------------------------------
	public Cache(int size, Logger logger, PeerDescriptor self) {
		super();
		this.self = self;
		this.size = size;
		this.logger = logger;
		costChangeFlag = false;
		this.cyclonPartners = new ArrayList<PeerDescriptor>();
		this.entries = new ArrayList<ViewEntry>();
		this.fingersList = new HashMap<Integer, PeerDescriptor>();
		this.children = new ArrayList<PeerDescriptor>();
		this.parents = new ArrayList<PeerDescriptor>();
		this.d2e = new HashMap<PeerAddress, ViewEntry>();
		random = new Random();
		random.setSeed(Global.seed);
		sortedMLs = new ArrayList<Integer>(Global.odDistr.keySet());
		Collections.sort(sortedMLs);
		if(self.getPeerAddress().isSource())
			nextML = 9999;
		else if(sortedMLs.indexOf(self.getMoney())!=sortedMLs.size()-1)
			nextML = sortedMLs.get(sortedMLs.indexOf(self.getMoney())+1);
		else
			nextML = Global.srcOD;
	}

//-------------------------------------------------------------------
	public void incrementDescriptorAges() {
		for (ViewEntry entry : entries) {
			entry.getDescriptor().incrementAndGetAge();
		}
	}

//-------------------------------------------------------------------
	public PeerAddress selectPeerToShuffleWith() {
		if (entries.isEmpty()) {
			return null;
		}
		ViewEntry oldestEntry = Collections.max(entries, comparatorByAge);
		if(selectViableEntries(oldestEntry.getDescriptor()).size()>0)
		{
			if(oldestEntry.getDescriptor().getPeerAddress().isSource())
				System.out.println("testingpoint");
			removeEntry(oldestEntry);
			return oldestEntry.getDescriptor().getPeerAddress();
		}
		else
			return null;
	}

//-------------------------------------------------------------------
	public ArrayList<PeerDescriptor> selectToSendAtActive(int count, PeerAddress destinationPeer) {
		ArrayList<ViewEntry> randomEntries = generateRandomSample(count, selectViableEntries(new PeerDescriptor(destinationPeer)));
		
		ArrayList<PeerDescriptor> descriptors = new ArrayList<PeerDescriptor>();
		
		for (ViewEntry cacheEntry : randomEntries) {
			cacheEntry.sentTo(destinationPeer);
			descriptors.add(cacheEntry.getDescriptor());
		}
		
		return descriptors;
	}

//-------------------------------------------------------------------
	public ArrayList<PeerDescriptor> selectToSendAtPassive(int count, PeerAddress destinationPeer) {
		ArrayList<ViewEntry> randomEntries = generateRandomSample(count, selectViableEntries(new PeerDescriptor(destinationPeer)));
		
		ArrayList<PeerDescriptor> descriptors = new ArrayList<PeerDescriptor>();
		
		for (ViewEntry cacheEntry : randomEntries) {
			cacheEntry.sentTo(destinationPeer);
			descriptors.add(cacheEntry.getDescriptor());
		}
		
		return descriptors;
	}

//-------------------------------------------------------------------
	public void MergeCyclonPartners(ArrayList<PeerDescriptor> descriptors, boolean joining) {
		cyclonPartners = descriptors;
		for (PeerDescriptor descriptor : descriptors) {
			if (self.equals(descriptor)) {
				// do not keep descriptor of self
				continue;
			}
			
			//Select cyclon partners to keep in the finger list
			if (descriptor.getMoney() > self.getPeerAddress().getBandwidth())
			{
				PeerDescriptor finger = fingersList.get(descriptor.getMoney());
				if(!(finger != null && finger.equals(descriptor) && finger.getAge()<= descriptor.getAge()))
					fingersList.put(descriptor.getMoney(), descriptor);
			}
			
			//Select cyclon partners to keep in the similar view
			//If node already exists, skip (and update age if necessary)
			if (d2e.containsKey(descriptor.getPeerAddress())) {
				// we already have an entry for this peer. keep the youngest one
				ViewEntry entry = d2e.get(descriptor.getPeerAddress());
				if (entry.getDescriptor().getAge() > descriptor.getAge()) {
					// we keep the lowest age descriptor
					removeEntry(entry);
					addEntry(new ViewEntry(descriptor));
					continue;
				} else {
					continue;
				}
			}
			if (joining)
			{
				//If this is the peer's first shuffle, keep every node that's gte to the peer's banddwidth
				if(descriptor.getMoney() >= self.getPeerAddress().getBandwidth())
				{
					if (entries.size() < size) {
						// fill an empty slot
						addEntry(new ViewEntry(descriptor));
						continue;
					}
				}
			}
			else
			{
				if (descriptor.getMoney() == self.getPeerAddress().getBandwidth() || descriptor.getMoney() == nextML)
				{
					if (entries.size() < size) {
						// fill an empty slot
						addEntry(new ViewEntry(descriptor));
						continue;
					}
					
					//Replace dissimilar nodes
					ViewEntry m = getDisimilarPeer();
					if(m != null)
					{
						removeEntry(m);
						addEntry(new ViewEntry(descriptor));
					}
				}
				
			}
		}	
	}
	
//-------------------------------------------------------------------
	public ArrayList<PeerDescriptor> selectToKeep(PeerAddress from, ArrayList<PeerDescriptor> descriptors) {
		LinkedList<ViewEntry> entriesSentToThisPeer = new LinkedList<ViewEntry>();
		for (ViewEntry cacheEntry : entries) {
			if (cacheEntry.wasSentTo(from)) {
				entriesSentToThisPeer.add(cacheEntry);
			}
		}

		for (PeerDescriptor descriptor : descriptors) {
			if(self.getPeerAddress().getBandwidth() == descriptor.getMoney() || nextML == descriptor.getMoney())
			{
				if (self.equals(descriptor)) {
					// do not keep descriptor of self
					continue;
				}
	
				if (d2e.containsKey(descriptor.getPeerAddress())) {
					// we already have an entry for this peer. keep the youngest one
					ViewEntry entry = d2e.get(descriptor.getPeerAddress());
					if (entry.getDescriptor().getAge() > descriptor.getAge()) {
						// we keep the lowest age descriptor
						removeEntry(entry);
						addEntry(new ViewEntry(descriptor));
						continue;
					} else {
						continue;
					}
				}
				
				if (entries.size() < size) {
					// fill an empty slot
					addEntry(new ViewEntry(descriptor));
					continue;
				}
				
				// replace one slot out of those sent to this peer
				ViewEntry sentEntry = entriesSentToThisPeer.poll();
				if (sentEntry != null) {
					removeEntry(sentEntry);
					addEntry(new ViewEntry(descriptor));
				}
			}
		}
		
		return selectPeersToAcquire(selectCandidates());
		
	}
	
//-------------------------------------------------------------------
	public final boolean decideOnChild(PeerDescriptor child)
	{
//		if(self.getPeerAddress().isSource())
//			System.out.println("I'm a source node");
		if(children.size() < self.getMoney())
		{
			children.add(child);
			reCalculatePrice();
			return true;
		}
		else if(children.size() != 0)
		{
			PeerDescriptor minChild = Collections.min(children, comparatorByMoney);
			if(child.getMoney()>minChild.getMoney())
			{
				childToBeRemoved = minChild;
				children.remove(minChild);
				children.add(child);
				reCalculatePrice();
				return true;
			}
		}
		return false;
	}

//-------------------------------------------------------------------
	public final boolean stillWantParent(PeerDescriptor parent)
	{
		if(parents.size()<Global.id)
		{
			parents.add(parent);
			reCalculateCost();
			return true;
		}
		else
		{
			
			//I find it more logical to compare "already acquired" parents using cost instead of net profit
			//If you want to revert back, uncomment the lines above
			PeerDescriptor minParent = Collections.max(parents, comparatorByCost);
			//System.out.println(minNP);
			if(parent.getCost()<minParent.getCost() || (parent.getCost() == minParent.getCost() && parent.getMoney() > minParent.getMoney()))
			{
				if(parent.equals(minParent))
					logger.error("Parent {} that I'm about to accept can't be equivalent to that which I'm about to reject", parent.getPeerAddress());
				parentToBeRemoved = minParent;
				parents.remove(minParent);
				parents.add(parent);
				reCalculateCost();
				return true;
			}
			return false;
		}
	}
	
//-------------------------------------------------------------------
	public final ArrayList<PeerDescriptor> getAllSimilarEntries() {
		ArrayList<PeerDescriptor> descriptors = new ArrayList<PeerDescriptor>();

		for (ViewEntry cacheEntry : entries)
			descriptors.add(cacheEntry.getDescriptor());
		
		return descriptors;
	}
	
//-------------------------------------------------------------------
	public final HashMap<Integer, PeerDescriptor> getAllFingers() {
		return fingersList;
	}
	
//-------------------------------------------------------------------
	public final ArrayList<PeerDescriptor> getAllCyclonPartners() {
		return cyclonPartners;
	}
	
//-------------------------------------------------------------------
	public final PeerDescriptor getSelf() {
		return self;
	}
	
//-------------------------------------------------------------------
	public final ArrayList<PeerDescriptor> getChildren() {
		return children;
	}
	
//-------------------------------------------------------------------
	public final ArrayList<PeerDescriptor> getParents() {
		return parents;
	}
//-------------------------------------------------------------------
	public final void removeChild(PeerDescriptor child) {
		children.remove(child);
		reCalculatePrice();
	}
		
//-------------------------------------------------------------------
	public final void removeParent(PeerDescriptor parent) {
//		if(!parents.contains(parent))
//		{
//			int count = 0;
//			System.out.println("You parent view consists of:");
//			for(PeerDescriptor peer: parents)
//			{
//				System.out.println("parent " + count + " : " + peer.getPeerAddress());
//				count++;
//			}
//			throw new RuntimeException("Peer " + parent.getPeerAddress() + " should be in your parents view, but it's not");
//		}

		parents.remove(parent);
		reCalculateCost();
	}
	
//-------------------------------------------------------------------
	public final void updateParent(PeerDescriptor parent) {
		int count=0;
		while(parents.contains(parent))
		{
			count++;
			parents.remove(parent);
		}
		if(count>0)
		{
			for(int i=0; i<count; i++)
			{
				parents.add(parent);
			}
			reCalculateCost();
		}
	}
	
//-------------------------------------------------------------------
	public final void updateViewEntry(PeerDescriptor peer)
	{
		int peerMoney = peer.getMoney();
		if(d2e.containsKey(peer.getPeerAddress()))
		{
			ViewEntry peerEntry = d2e.get(peer.getPeerAddress());
			peerEntry.updateDescriptor(peer);
		}
		
		if(peer.equals(fingersList.get(peerMoney)))
		{
			fingersList.put(peerMoney, peer);
		}
	}

//-------------------------------------------------------------------
	public final List<PeerAddress> getRandomPeers(int count) {
		ArrayList<ViewEntry> randomEntries = generateRandomSample(count, entries);
		LinkedList<PeerAddress> randomPeers = new LinkedList<PeerAddress>();

		for (ViewEntry cacheEntry : randomEntries) {
			randomPeers.add(cacheEntry.getDescriptor().getPeerAddress());
		}

		return randomPeers;
	}

//-------------------------------------------------------------------
	public boolean hasCostChanged()
	{
		if(costChangeFlag == true)
		{
			costChangeFlag = false;
			return true;
		}
		else
			return costChangeFlag;
	}

//-------------------------------------------------------------------
	public PeerAddress getChildToBeRemoved()
	{
		if(childToBeRemoved == null)
			return null;
		PeerAddress c = childToBeRemoved.getPeerAddress();
		childToBeRemoved = null;
		return c;
	}
	
//-------------------------------------------------------------------
	public PeerAddress getParentToBeRemoved()
	{
		if(parentToBeRemoved == null)
			return null;
		PeerAddress p = parentToBeRemoved.getPeerAddress();
		parentToBeRemoved = null;
		return p;
	}
	
//-------------------------------------------------------------------
    public float getAverageSimilarAge()
    {
    	Float total = (float) 0;
    	for(ViewEntry ve : entries)
    	{
    		total = total + ve.getDescriptor().getAge();
    	}
    	if(entries.size() != 0)
    		return total/entries.size();
    	else
    		return (float) 0;
    }
    
//-------------------------------------------------------------------
    public float getAverageCyclonAge()
    {
    	Float total = (float) 0;
    	for(PeerDescriptor pd : cyclonPartners)
    	{
    		total = total + pd.getAge();
    	}
    	if(cyclonPartners.size() != 0)
    		return total/cyclonPartners.size();
    	else
    		return (float) 0;
    }
	
//-------------------------------------------------------------------
	private final ArrayList<ViewEntry> generateRandomSample(int n, ArrayList<ViewEntry> entries) {
		ArrayList<ViewEntry> randomEntries;
		if (n >= entries.size()) {
			// return all entries
			randomEntries = new ArrayList<ViewEntry>(entries);
		} else {
			// return count random entries
			randomEntries = new ArrayList<ViewEntry>();
			// Don Knuth, The Art of Computer Programming, Algorithm S(3.4.2)
			int t = 0, m = 0, N = entries.size();
			while (m < n) {
				int x = random.nextInt(N - t);
				if (x < n - m) {
					randomEntries.add(entries.get(t));
					m += 1;
					t += 1;
				} else {
					t += 1;
				}
			}
		}
		return randomEntries;
	}

//-------------------------------------------------------------------
	private void addEntry(ViewEntry entry) {
		entries.add(entry);
		d2e.put(entry.getDescriptor().getPeerAddress(), entry);
		checkSize();
	}

//-------------------------------------------------------------------
	private void removeEntry(ViewEntry entry) {
		entries.remove(entry);
		d2e.remove(entry.getDescriptor().getPeerAddress());
		checkSize();
	}
	
//-------------------------------------------------------------------
	private ViewEntry getDisimilarPeer()
	{
		for(ViewEntry v: entries)
		{
			if(v.getDescriptor().getMoney()>nextML || v.getDescriptor().getMoney() < self.getPeerAddress().getBandwidth())
				return v;
		}
		return null;
	}
//-------------------------------------------------------------------
	private ArrayList<PeerDescriptor> selectCandidates()
	{
		ArrayList<PeerDescriptor> candidates = new ArrayList<PeerDescriptor>();
		float maxCost;
		if(parents.size()<Global.id)
			maxCost = 10000;
		else
			maxCost = Collections.max(parents, comparatorByCost).getCost();
		for(ViewEntry entry: entries)
		{
			if(entry.getDescriptor().getCost()<maxCost && entry.getDescriptor().getPrice()<self.getMoney() && !children.contains(entry.getDescriptor()) && entry.getDescriptor().getMoney()!=0)
				candidates.add(entry.getDescriptor());
		}
		for(PeerDescriptor pd: fingersList.values())
		{
//			if(pd.getPeerAddress().isSource())
//				System.out.println("source node in my fingerslist, max cost= " + maxCost);
			if(pd.getCost()<maxCost && pd.getPrice()<self.getMoney() && !children.contains(pd))
				candidates.add(pd);
		}
		return candidates;
	}
	
//-------------------------------------------------------------------
	private ArrayList<PeerDescriptor> selectPeersToAcquire(ArrayList<PeerDescriptor> candidates)
	{
		Collections.sort(candidates, comparatorByNetProfit);
		ArrayList<PeerDescriptor> peersToAcquire = new ArrayList<PeerDescriptor>();
		//For now we'll only attempt to acquire 1 peer per shuffle
		if(candidates.size() != 0)
			peersToAcquire.add(candidates.get(candidates.size()-1));
		//Uncomment the line below to attempt to acquire all candidates instead
//		return candidates;
		return peersToAcquire;
	}
	
//-------------------------------------------------------------------
	private void reCalculateCost()
	{
		//This part might need to be revised
		float oldCost = self.getCost();
		int oldSp = self.getSP();
		int diff = Global.id - parents.size();
		float newCost = 0;
		int newSp = 9999;
		for(PeerDescriptor p: parents)
		{
			newCost += p.getSP();
			if(newSp>p.getSP())
				newSp = p.getSP();
		}
		newCost += 9999*diff;
		newCost = newCost/(float)Global.id;
		
		if(newCost != 9999)
			newCost++;
		
		if(newSp != 9999)
			newSp++;
		
		if(newCost != oldCost || newSp != oldSp)
		{
			costChangeFlag = true;
			self.setCost(newCost);
			self.setSP(newSp);
		}
	}
	
//-------------------------------------------------------------------
	private void reCalculatePrice()
	{
		if(self.getMoney()>children.size())
			self.setPrice(-1);
		else
		{
			int min=9999;
			for(PeerDescriptor c: children)
			{
				if(c.getMoney()<min)
					min= c.getMoney();
			}
			self.setPrice(min);
		}
		
	}
	
	//-------------------------------------------------------------------
	
	//Select entries that can be sent to a certain peer
	private ArrayList<ViewEntry> selectViableEntries(PeerDescriptor randomPeer)
	{
		//Should calculate the next market level for the randomPeer
		int randomPeerNextML;
		if(randomPeer.getPeerAddress().isSource())
			randomPeerNextML = 9999;
		else if(sortedMLs.indexOf(randomPeer.getMoney())!=sortedMLs.size()-1)
			randomPeerNextML = sortedMLs.get(sortedMLs.indexOf(randomPeer.getMoney())+1);
		else
			randomPeerNextML = Global.srcOD;
		ArrayList<ViewEntry> viableEntries = new ArrayList<ViewEntry>();
		for(ViewEntry entry: entries)
		{
			if(!randomPeer.equals(entry.getDescriptor()))
			{
				if(entry.getDescriptor().getMoney()==randomPeer.getMoney() || entry.getDescriptor().getMoney()==randomPeerNextML)
					viableEntries.add(entry);
			}
		}
		return viableEntries;
	}

//-----------------------------------------------------------------------
	private void checkSize() {
		if (entries.size() != d2e.size())
		{
			for(ViewEntry entry: entries)
			{
				System.out.println("Entry Address: " + entry.getDescriptor().getPeerAddress());
			}
			for(PeerAddress address: d2e.keySet())
			{
				System.out.println("d2e Address: " + address);
			}
			throw new RuntimeException("WHD " + entries.size() + " <> " + d2e.size());
		}
	}
}
